package com.mxx.common.data.jpa.auditing;

import org.springframework.data.annotation.*;
import org.springframework.data.auditing.MappingAuditableBeanWrapperFactory;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyPaths;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.util.Lazy;
import org.springframework.util.Assert;

import java.lang.annotation.Annotation;
import java.util.function.Predicate;
import java.util.stream.Stream;

public class MappingAuditingMetadata {

    private static final Predicate<? super PersistentProperty<?>> HAS_COLLECTION_PROPERTY = it -> it.isCollectionLike()
            || it.isMap();

    public final PersistentPropertyPaths<?, ? extends PersistentProperty<?>> createdByPaths, createdDatePaths,
            lastModifiedByPaths, lastModifiedDatePaths, versionPaths, javaxVersionPaths, deleteFlagPaths, deleteByPaths, deleteTimePaths;

    private final Lazy<Boolean> isAuditable;

    /**
     * Creates a new {@link MappingAuditableBeanWrapperFactory.MappingAuditingMetadata} instance from the given {@link PersistentEntity}.
     *
     * @param entity must not be {@literal null}.
     */
    public <P> MappingAuditingMetadata(MappingContext<?, ? extends PersistentProperty<?>> context, Class<?> type) {

        Assert.notNull(type, "Type must not be null!");

        this.createdByPaths = findPropertyPaths(type, CreatedBy.class, context);
        this.createdDatePaths = findPropertyPaths(type, CreatedDate.class, context);
        this.lastModifiedByPaths = findPropertyPaths(type, LastModifiedBy.class, context);
        this.lastModifiedDatePaths = findPropertyPaths(type, LastModifiedDate.class, context);
        this.versionPaths = findPropertyPaths(type, Version.class, context);
        this.deleteFlagPaths = findPropertyPaths(type, DeleteFlag.class, context);
        this.deleteByPaths = findPropertyPaths(type, DeleteBy.class, context);
        this.deleteTimePaths = findPropertyPaths(type, DeleteTime.class, context);
        this.javaxVersionPaths = findPropertyPaths(type, javax.persistence.Version.class, context);

        this.isAuditable = Lazy.of( //
                () -> //
                        Stream.of(createdByPaths, createdDatePaths, lastModifiedByPaths, lastModifiedDatePaths) //
                                .anyMatch(it -> !it.isEmpty())//
        );
    }

    /**
     * Returns whether the {@link PersistentEntity} is auditable at all (read: any of the auditing annotations is
     * present).
     *
     * @return
     */
    public boolean isAuditable() {
        return isAuditable.get();
    }

    private PersistentPropertyPaths<?, ? extends PersistentProperty<?>> findPropertyPaths(Class<?> type,
                                                                                          Class<? extends Annotation> annotation, MappingContext<?, ? extends PersistentProperty<?>> context) {
        return context //
                .findPersistentPropertyPaths(type, withAnnotation(annotation)) //
                .dropPathIfSegmentMatches(HAS_COLLECTION_PROPERTY);
    }

    private static Predicate<PersistentProperty<?>> withAnnotation(Class<? extends Annotation> type) {
        return t -> t.findAnnotation(type) != null;
    }
}
